import os
import shutil
import zipfile
from copy import deepcopy, copy
from pathlib import Path
from time import time
from typing import (Union, Optional, Sequence, Generator, Callable, List,
                    Literal, Set)

from filetype import guess, types as file_types
from tqdm import tqdm

from .log import logger
from .decorator import param2path, timestamp
from .utils import x2path

RENAME_TYPE = Literal['name', 'stem', 'suffix']
REPETITION_TYPE = Literal['a', 'all', 'p', 'part']
KEY_TYPE = RENAME_TYPE
C_TYPE = Literal['zip', 'tar']


class Folder(object):

    @timestamp
    @param2path({'p': 1})
    def __init__(self, p: Union[str, Path, List] = '',
                 d: Sequence = None) -> None:
        """

        Args:
            p: 文件夹地址
            d: 初始数据
        """
        self._path_c = p
        self._data = []  # 数据容器
        self._data_unique = []
        self._data_set = set()
        # 获取路径下所有文件路径对象
        if d is None:
            if p.is_file():
                self._data = [p]
            else:
                _s = time()
                self._data = list(self._path_c.rglob('*'))
                print(time() - _s)
        else:
            for _p in list(d):
                self._set_data(_p)
        # 去除重复 慢
        # if len(self) > len(self.data_set):
        #     logger.info(
        #         f"数据存在重复：{len(self) - len(self.data_set)}")
        #     self._data = [Path(_str) for _str in self.data_unique]
        logger.info(f"数据总数：{len(self)}")

    def __iter__(self) -> Generator[Path, None, None]:
        for _i, p in enumerate(self._data):
            if not isinstance(p, Path):
                p = x2path(p)
                self._data[_i] = p
            yield p

    def __len__(self) -> int:
        return len(self._data)

    def __getitem__(self, item) -> Union['Folder', Path]:
        if isinstance(item, slice):
            return self.__class__(self._path_c, self._data[item])
        elif isinstance(item, int):
            return self._data[item]
        else:
            return self

    def __add__(self, other: 'Folder') -> 'Folder':
        self._data += list(other)
        return self

    def __sub__(self, other: 'Folder') -> 'Folder':
        if self is other:
            logger.warn(f"相减的对象是同一个对象")
        _data = list(self.data_set - other.data_set)
        _data.sort(key=list(self.data_str).index)
        return self.__class__(self._path_c, _data)

    @property
    def data_str(self) -> Generator:
        for _p in self._data:
            yield str(_p.resolve())

    @property
    def data_unique(self) -> Generator:
        """
        去重后按源数据顺序输出
        Returns:

        """
        if not self._data_unique:
            self._data_unique = list(self.data_set)
            self._data_unique.sort(key=list(self.data_str).index)
        yield from self._data_unique

    @property
    def data_set(self) -> Set:
        if not self._data_set:
            self._data_set = set(self.data_str)
        return self._data_set

    @param2path({'p': 1})
    def _set_data(self, p: Union[str, Path, List]) -> None:
        """
        设置数据内容
        Args:
            p:

        Returns:

        """
        self._data.append(p)

    @classmethod
    @param2path({'p': 1})
    def folder_delete(cls, p: Path) -> None:
        """
        删除文件夹
        Args:
            p: 文件夹路径对象

        Returns:

        """
        if not p.exists():
            return
        dir_list = list(p.iterdir())  # 文件夹列表
        if not dir_list:
            dir_list = []
        for p_c in dir_list:
            if p_c.is_dir():
                cls.folder_delete(p_c)
            else:
                p_c.unlink(True)
        p.rmdir()

    @classmethod
    @param2path({'p': 1})
    def is_valid(cls, p: Union[Path, str, Sequence]) -> bool:
        """
        判断文件或文件夹是否有效，边界：是存在一个有内容的文件
        Args:
            p: 文件或文件夹路径

        Returns:

        """
        # 文件夹
        if p.is_dir():
            _dirs = list(p.iterdir())
            # 文件不为空
            if len(_dirs) > 0:
                for _p in _dirs:
                    if cls.is_valid(_p):
                        return True
        # 文件
        else:
            if os.path.getsize(p) > 0:
                return True

        return False

    @timestamp
    def repetition(self, k: KEY_TYPE = 'name', t: REPETITION_TYPE = 'all',
                   verify_size: bool = False,
                   ) -> 'Folder':
        """
        重复文件
        Args:
            k: 查重的键 全名：n|name 简称：s|stem 后缀：suf|suffix
            t: 保留重复文件类型 所有：a|all 重复部分：p|part
            verify_size: 是否校验文件大小

        Returns:

        """
        _statistics = {}  # 数据字典
        _repetition = []  # 重复数据键
        # 遍历数据获取文件名称类似文件
        for _p in self:
            if _p.is_dir():
                continue
            if k in ['n', 'name']:
                _k = _p.name.lower()  # 名称 + 后缀 转小写
            elif k in ['suf', 'suffix']:
                _k = _p.suffix.lower()
            else:
                _k = _p.stem.lower()

            if _k in _statistics:
                _statistics[_k].append(_p)
                if _k not in _repetition:
                    _repetition.append(_k)  # 记录重复键
            else:
                _statistics[_k] = [_p]

        # 从名称重复的文件中筛选出文件大小相同的文件
        if verify_size:
            _repetition_new = []  # 新重复键列表
            for _k in _repetition:
                _size_dict = {}  # 文件大小
                _repetition_size = []  # 大小重复键
                for _p in _statistics[_k]:
                    _size = os.path.getsize(_p)  # 获取文件大小
                    # 记录
                    if _size in _size_dict:
                        _size_dict[_size].append(_p)
                        if _size not in _repetition_size:
                            _repetition_size.append(_size)
                    else:
                        _size_dict[_size] = [_p]
                # 更新重复数据记录
                for _s in _repetition_size:
                    _k_s = f"{_k}_{_s}"
                    _statistics[_k_s] = _size_dict.get(_s, [])
                    _repetition_new.append(_k_s)
            _repetition = _repetition_new  # 更新重复键列表

        if t in ['p', 'part']:
            # 剔除重复数据的第一个数据
            _data = [_p for _k in _repetition for _p in _statistics[_k][1:]]
        else:
            _data = [_p for _k in _repetition for _p in _statistics[_k]]

        self._data = _data  # 赋值

        return self

    @timestamp
    def copy(self, is_deep: bool = False) -> 'Folder':
        """
        拷贝对象
        Args:
            is_deep: 是否深拷贝

        Returns:
            Folder 对象
        """
        return deepcopy(self) if is_deep else copy(self)

    @timestamp
    def filter(self, call_back: Callable[[Path], bool]) -> 'Folder':
        """
        获取符合条件的所有文件或文件夹
        Args:
            call_back: 回调

        Returns:
            Folder 对象
        """
        self._data = list(filter(call_back, self.copy()))
        return self

    @timestamp
    def map(self, call_back: Callable[[Path], Optional[Path]]) -> 'Folder':
        """
        遍历修改数据
        Args:
            call_back:

        Returns:

        """
        return Folder(self._path_c, list(map(call_back, self.copy())))

    def each(self, call_back: Callable[[Path], None]) -> 'Folder':
        """
        遍历所有数据
        Args:
            call_back:

        Returns:

        """
        for p_c in self:
            call_back(copy(p_c))
        return self

    @timestamp
    def sort(self, reverse=True) -> 'Folder':
        """
        地址排序
        Args:
            reverse: 排序规则，reverse = True 降序 ， reverse = False 升序

        Returns:

        """
        self._data = sorted(self._data,
                            key=lambda p: len(p.parts),
                            reverse=reverse)
        return self

    @timestamp
    def valid(self, is_valid: bool = True) -> 'Folder':
        """
        判断数据是否有效
        Args:
            is_valid: 是否有效

        Returns:

        """
        _valid = []  # 有效列表
        _invalid = []  # 无效列表
        for _p in self.copy().sort():
            if self.is_valid(_p):
                _valid.append(_p)
            else:
                _invalid.append(_p)
        # 数据回填
        if is_valid:
            self._data = _valid
        else:
            self._data = _invalid

        return self

    @timestamp
    def delete(self, is_valid: bool = False) -> 'Folder':
        """
        删除无效或所有 文件及文件夹
        Args:
           is_valid: 只删除无效文件或文件夹

        Returns:

        """
        if is_valid:
            _delete_data = self.copy()
            self._data = []  # 清空数据
            _msg = f"删除 所有 文件及文件夹进度"
        else:
            _delete_data = self.copy().valid(False).sort()  # 获取无效数据列表
            _msg = f"删除 无效 文件及文件夹进度"

        with tqdm(total=len(_delete_data)) as p_bar:
            p_bar.set_description(_msg)
            for _p in _delete_data:
                if _p.is_dir():
                    _delete_data.folder_delete(_p)
                else:
                    _p.unlink(True)
                p_bar.update(1)

        return self

    @timestamp
    @param2path({'out_path': 1}, ['out_path'], 0)
    def move(self, out_path: [str, Path] = Path(r''), is_copy: bool = True,
             is_rename: bool = True) -> 'Folder':
        """
        移动所有文件到指定路径下
        Args:
            out_path: 指定输出路径
            is_copy: 是否拷贝模式
            is_rename: 重名是否重命名

        Returns:
            Folder 对象
        """
        out_path_c = out_path
        with tqdm(total=len(self)) as p_bar:
            for p_c in self:
                p_bar.set_description(f"移动文件进度")
                file_out_c = out_path_c.joinpath(p_c.name)  # 移动后的文件地址
                if p_c.is_file():
                    if file_out_c.exists():
                        if is_rename:
                            file_out_c = file_out_c.with_name(
                                f"{file_out_c.stem}"
                                f"_{int(time() * 1000)}"
                                f"{file_out_c.suffix}")
                        else:
                            logger.warn(f"文件已存在：{file_out_c.absolute()}")
                            p_bar.update(1)
                            continue

                    if is_copy:
                        shutil.copy(p_c, file_out_c)
                    else:
                        p_c.replace(file_out_c)
                else:
                    if not file_out_c.exists():
                        if is_copy:
                            shutil.copytree(p_c, file_out_c)
                        else:
                            p_c.replace(file_out_c)

                p_bar.update(1)

        return self

    @timestamp
    @param2path({'out_path': 1}, ['out_path'], 0)
    def compress(self, out_path: Union[str, Path] = r'.', name: str = '',
                 c_type: C_TYPE = 'zip') -> 'Folder':
        """
        所有文件添加到 zip 包
        Args:
            out_path: 压缩包输出地址
            name: 压缩包名称
            c_type: 压缩类型

        Returns:
            Folder 对象
        """
        out_path_c = out_path

        if not name:
            name = 'package'

        with tqdm(total=len(self)) as p_bar:
            p_bar.set_description('压缩文件进度')
            if c_type.lower() == 'zip':
                out_path_c = out_path_c.joinpath(f"{name}.zip")
                with zipfile.ZipFile(str(out_path_c), mode='w',
                                     compression=zipfile.ZIP_STORED) as f:
                    for p_c in self:
                        f.write(str(p_c), arcname=p_c.name)
                        p_bar.update(1)
            else:
                raise Exception(f"不支持压缩类型：{c_type}")

        return self

    @timestamp
    def rename(self, names: Union[Sequence[str], str], t: RENAME_TYPE = 'stem',
               is_update: bool = False) -> 'Folder':
        """
        文件更名
        Args:
            names: 新的文件名序列
            t: 更名类型 全名：n|name 简称：s|stem 后缀：suf|suffix（注：要以.开头）
            is_update: 是否修改本地文件名

        Returns:

        """
        _len = len(self)
        _zfill = len(str(_len))

        if t in ['suf', 'suffix']:
            _func = 'with_suffix'
            if isinstance(names, str):
                names = [names] * _len
        elif t in ['s', 'stem']:
            _func = 'with_stem'
            if isinstance(names, str):
                names = [f"{names}_{str(_i).zfill(_zfill)}"
                         for _i in range(_len)]
        else:
            _func = 'with_name'
            if isinstance(names, str):
                _s, _suf = os.path.splitext(names)
                names = [f"{_s}_{str(_i).zfill(_zfill)}{_suf}"
                         for _i in range(_len)]
        if is_update:
            self._data = [p.rename(getattr(p, _func)(n))
                          for p, n in zip(self._data, names)]
        else:
            self._data = [getattr(p, _func)(n)
                          for p, n in zip(self._data, names)]
        return self

    @timestamp
    def rename_by_func(self, call_back: Callable[[str], str],
                       t: RENAME_TYPE = 'stem', is_update: bool = False
                       ) -> 'Folder':
        """
        文件更名 通过回调函数
        Args:
            call_back: 回调
            t: 更名类型 全名：n|name 简称：s|stem 后缀：suf|suffix
            is_update: 是否修改本地文件名

        Returns:

        """
        names = []
        for _p in self._data:
            if t in ['suf', 'suffix']:
                _input = _p.suffix
            elif t in ['s', 'stem']:
                _input = _p.stem
            else:
                _input = _p.name
            _name = call_back(_p)
            names.append(
                _name if bool(_name) and isinstance(_name, str) else _input)

        return self.rename(names, t, is_update)

    def is_file(self) -> 'Folder':
        """
        保留文件

        Returns:
            Folder 对象
        """
        return self.filter(lambda p: p.is_file())

    def is_dir(self) -> 'Folder':
        """
        保留文件夹

        Returns:
            Folder 对象
        """
        return self.filter(lambda p: p.is_dir())

    def print(self) -> 'Folder':
        """
        遍历打印
        Returns:

        """
        self.each(lambda a: print(str(Path(a).absolute())))
        return self
